﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-07-24 19:00
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using BassDrop.Core.Common.Utilities;
using BassDrop.Core.Common.Plugins;
using BassDrop.Core.Common.Data.IO.Portable;
using BassDrop.Core.Common.Services;
using BassDrop.Core.Common.Cloud.Assets;
using System.Threading.Tasks;

namespace BassDrop.Core.Common.Cloud
{
	/// <summary>
	/// Base class for cloud communications
	/// </summary>
	public abstract class CloudConnectionBase : PluginObjectBase
	{
		/// <summary>
		/// Used to notify all cloud providers that they are no longer the default one
		/// </summary>
		private static event EventHandler UnsetDefault;

		/// <summary>
		/// The ID of this client
		/// </summary>
		public virtual string ClientID
		{
			get
			{
				return Controller.MachineId;
			}
		}

		/// <summary>
		/// The vector clocks in the system
		/// </summary>
		public virtual VectorClock Clocks
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets if this cloud provider is the default one
		/// </summary>
		public virtual bool IsDefault
		{
			get
			{
				return ((CloudOptionsSetBase) Options).IsDefault;
			}
		}

		/// <summary>
		/// Returns if the provider is ready to serve requests
		/// </summary>
		public abstract bool IsAvailable
		{
			get;
		}

		/// <summary>
		/// Protected constructor to set up the base instance
		/// </summary>
		/// <param name="pluginDescriptor">The descriptor object of the plugin owning this object</param>
		/// <param name="instanceName">the name of this plugin instance</param>
		protected CloudConnectionBase( BassDropPluginBase pluginDescriptor, string instanceName )
			: base( pluginDescriptor, instanceName )
		{
			Clocks = new VectorClock( Controller.MachineId );

			UnsetDefault += CloudConnectionBase_UnsetDefault;
		}

		/// <summary>
		/// Makes this cloud provider the default one
		/// </summary>
		public virtual void MakeDefault()
		{
			UnsetDefault( this, new EventArgs() );

			((CloudOptionsSetBase) Options).IsDefault = true;
		}

		/// <summary>
		/// Unsets this cloud provider from being the default one
		/// </summary>
		protected virtual void UnmakeDefault()
		{
			((CloudOptionsSetBase) Options).IsDefault = false;
		}

		private void CloudConnectionBase_UnsetDefault( object sender, EventArgs e )
		{
			UnmakeDefault();
		}

		#region Requests

		/// <summary>
		/// Update the vector clocks for the passed connection
		/// </summary>
		protected virtual async Task UpdateClocks()
		{
			Clocks.Increment();

			string tempPath = ServiceCenter.FileHandler.GetTempFileName();

			try
			{
				ServiceCenter.FileHandler.WriteAllText( tempPath, Clocks.ToVectorClockText() );
				await DoPut( CloudManager.ClocksDir + "/" + ClientID + ".vcf", new Uri( tempPath ) );

				foreach ( var item in await DoFind( CloudManager.ClocksDir, new Regex( ".*" ), false ) )
				{
					await DoGet( item, new Uri( tempPath ) );
					Clocks.Update( VectorClock.Parse( ServiceCenter.FileHandler.ReadAllText( tempPath ) ) );
				}
			}
			finally
			{
				ServiceCenter.FileHandler.Delete( tempPath );
			}
		}

		/// <summary>
		/// Returns the account information for this cloud connection (name, quotas, etc.)
		/// </summary>
		/// <returns></returns>
		public abstract Task<AccountInfo> GetAccountInfo();

		/// <summary>
		/// Copies a file from the cloud to the local filesystem.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		public virtual async Task Get( string remotePath, Uri localPath )
		{
			//await UpdateClocks(); // TODO: Get-be kell UpdateClocks?
			await DoGet( remotePath, localPath );
			ServiceCenter.FileHandler.SetMetadata( localPath.LocalPath, await GetMetadata( remotePath ) );
		}

		/// <summary>
		/// Reads the contents of a file in the cloud as text
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>the contents of the file as text</returns>
		public virtual async Task<string> GetText( string remotePath )
		{
			string tempPath = ServiceCenter.FileHandler.GetTempFileName();

			try
			{
				await Get( remotePath, new Uri( tempPath ) );

				return ServiceCenter.FileHandler.ReadAllText( tempPath );
			}
			finally
			{
				ServiceCenter.FileHandler.Delete( tempPath );
			}
		}

		/// <summary>
		/// Copies a file from the local filesystem to the cloud.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		public virtual async Task Put( string remotePath, Uri localPath )
		{
			if ( !await UnMarkDeleted( remotePath ) )
			{
				await UpdateClocks();
			}
			await DoPut( remotePath, localPath );
		}

		/// <summary>
		/// Puts a text file in the cloud with the specified text in it
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="text">text to put in the file</param>
		public virtual async Task PutText( string remotePath, string text )
		{
			string tempPath = ServiceCenter.FileHandler.GetTempFileName();

			ServiceCenter.FileHandler.WriteAllText( tempPath, text );

			try
			{
				await Put( remotePath, new Uri( tempPath ) );
			}
			finally
			{
				ServiceCenter.FileHandler.Delete( tempPath );
			}
		}

		/// <summary>
		/// Checks if a file exists in the cloud.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>does the file exist</returns>
		public virtual async Task<bool> Exists( string remotePath )
		{
			return await DoExists( remotePath );
		}

		/// <summary>
		/// Returns a list of file paths matching a regular expression from baseDir.
		/// </summary>
		/// <param name="baseDir">path to the base directory of the search</param>
		/// <param name="regex">regular expression to match</param>
		/// <param name="recursive">true to search subfolders</param>
		/// <returns></returns>
		public virtual async Task<List<string>> Find( string baseDir, Regex regex, bool recursive )
		{
			return await DoFind( baseDir, regex, recursive );
		}

		/// <summary>
		/// Returns the metadata of the passed file
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		public virtual async Task<FileMetadata> GetMetadata( string path )
		{
			return await DoGetMetadata( path );
		}

		/// <summary>
		/// Moves/renames a file inside the cloud. Overwriting existing files is not allowed.
		/// </summary>
		/// <param name="oldPath">the old path to the file in the cloud</param>
		/// <param name="newPath">the new (desired) path to the file in the cloud</param>
		public virtual async Task Move( string oldPath, string newPath )
		{
			if ( !await UnMarkDeleted( newPath ) )
			{
				await UpdateClocks();
			}
			await DoMove( oldPath, newPath );
		}

		/// <summary>
		/// Deletes a file from the cloud if it exists.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		public virtual async Task Delete( string remotePath )
		{
			await UpdateClocks();
			await DoDelete( remotePath );
		}

		/// <summary>
		/// Marks a file deleted in the cloud if it exists.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		public virtual async Task MarkDeleted( string remotePath )
		{
			if ( await Exists( remotePath ) )
			{
				await Delete( remotePath );
				await PutText( Registry.ToDeletedPath( remotePath ), Clocks.ToVectorClockText() );
			}
		}

		/// <summary>
		/// Marks a file not deleted in the cloud if it is marked deleted.
		/// This does not restore the file.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>did it do anything</returns>
		public virtual async Task<bool> UnMarkDeleted( string remotePath )
		{
			string deletedPath = Registry.ToDeletedPath( remotePath );
			if ( await Exists( deletedPath ) )
			{
				await Delete( deletedPath );
				return true;
			}

			return false;
		}

		/// <summary>
		/// Checks if a file is marked deleted (it has been in the cloud but since deleted). 
		/// It is possible, that a file exists yet it is marked deleted, but that generally suspects the wrong use of the API.
		/// It is also possible, that a file is deleted, yet it isn't marked so if it has been deleted with Delete() instead of
		/// MarkDeleted().
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>is the file marked as deleted</returns>
		public virtual async Task<bool> IsMarkedDeleted( string remotePath )
		{
			return await Exists( Registry.ToDeletedPath( remotePath ) );
		}

		/// <summary>
		/// Checks if a file is marked deleted (it has been in the cloud but since deleted) and returns the deletion metadata if it is. 
		/// It is possible, that a file exists yet it is marked deleted, but that generally suspects the wrong use of the API.
		/// It is also possible, that a file is deleted, yet it isn't marked so if it has been deleted with Delete() instead of
		/// MarkDeleted().
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>the metadata of the deleted file, shows when it was deleted or null if it isn't marked deleted</returns>
		public virtual async Task<FileMetadata> GetDeletionMetadata( string remotePath )
		{
			string deletedPath = Registry.ToDeletedPath( remotePath );
			if ( await Exists( deletedPath ) )
			{
				return await GetMetadata( deletedPath );
			}

			return null;
		}

		/// <summary>
		/// Copies a file from the cloud to the local filesystem.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		protected abstract Task DoGet( string remotePath, Uri localPath );

		/// <summary>
		/// Copies a file from the local filesystem to the cloud.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		protected abstract Task DoPut( string remotePath, Uri localPath );

		/// <summary>
		/// Checks if a file exists in the cloud.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>does the file exist</returns>
		protected abstract Task<bool> DoExists( string remotePath );

		/// <summary>
		/// Returns a list of file paths matching a regular expression from baseDir.
		/// </summary>
		/// <param name="baseDir">path to the base directory of the search</param>
		/// <param name="regex">regular expression to match</param>
		/// <param name="recursive">true to search subfolders</param>
		/// <returns></returns>
		protected abstract Task<List<string>> DoFind( string baseDir, Regex regex, bool recursive );

		/// <summary>
		/// Returns the metadata of the passed file
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		protected abstract Task<FileMetadata> DoGetMetadata( string path );

		/// <summary>
		/// Moves/renames a file inside the cloud. Overwriting existing files is not allowed.
		/// </summary>
		/// <param name="oldPath">the old path to the file in the cloud</param>
		/// <param name="newPath">the new (desired) path to the file in the cloud</param>
		protected abstract Task DoMove( string oldPath, string newPath );

		/// <summary>
		/// Deletes a file from the cloud if it exists.
		/// </summary>
		/// <param name="remotePath">path to the file in the cloud</param>
		protected abstract Task DoDelete( string remotePath );


		/// <summary>
		/// Copies a file from the cloud to the local filesystem.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		protected static async Task DoGet( CloudConnectionBase connection, string remotePath, Uri localPath )
		{
			await connection.DoGet( remotePath, localPath );
		}

		/// <summary>
		/// Copies a file from the local filesystem to the cloud.
		/// Overwrites any existing files.
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <param name="localPath">path to the file in the local filesystem</param>
		protected static async Task DoPut( CloudConnectionBase connection, string remotePath, Uri localPath )
		{
			await connection.DoPut( remotePath, localPath );
		}

		/// <summary>
		/// Checks if a file exists in the cloud.
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="remotePath">path to the file in the cloud</param>
		/// <returns>does the file exist</returns>
		protected static async Task<bool> DoExists( CloudConnectionBase connection, string remotePath )
		{
			return await connection.DoExists( remotePath );
		}

		/// <summary>
		/// Returns a list of file paths matching a regular expression from baseDir.
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="baseDir">path to the base directory of the search</param>
		/// <param name="regex">regular expression to match</param>
		/// <param name="recursive">true to search subfolders</param>
		/// <returns></returns>
		protected static async Task<List<string>> DoFind( CloudConnectionBase connection, string baseDir, Regex regex, bool recursive )
		{
			return await connection.DoFind( baseDir, regex, recursive );
		}

		/// <summary>
		/// Moves/renames a file inside the cloud. Overwriting existing files is not allowed.
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="oldPath">the old path to the file in the cloud</param>
		/// <param name="newPath">the new (desired) path to the file in the cloud</param>
		protected static async Task DoMove( CloudConnectionBase connection, string oldPath, string newPath )
		{
			await connection.DoMove( oldPath, newPath );
		}

		/// <summary>
		/// Deletes a file from the cloud if it exists.
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="remotePath">path to the file in the cloud</param>
		protected static async Task DoDelete( CloudConnectionBase connection, string remotePath )
		{
			await connection.DoDelete( remotePath );
		}

		/// <summary>
		/// Returns the metadata of the passed file
		/// </summary>
		/// <param name="connection">the connection to run the request on</param>
		/// <param name="path"></param>
		/// <returns></returns>
		protected static async Task<FileMetadata> DoGetMetadata( CloudConnectionBase connection, string path )
		{
			return await connection.DoGetMetadata( path );
		}

		#endregion Requests
	}
}
